﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

namespace Yapp
{
    public class PhysicsSimulation : ScriptableObject
    {

        #region Public Editor Fields

        public int maxIterations = 1000;
        public Vector2 forceMinMax = Vector2.zero;
        public float forceAngleInDegrees = 0f;
        public bool randomizeForceAngle = false;

        #endregion Public Editor Fields

        private SimulatedBody[] simulatedBodies;

        private List<Rigidbody> generatedRigidbodies;
        private List<Collider> generatedColliders;

        public void RunSimulation(Transform[] gameObjects)
        {
            AutoGenerateComponents(gameObjects);

            simulatedBodies = gameObjects.Select(rb => new SimulatedBody(rb)).ToArray();

            Simulate(simulatedBodies);

            RemoveAutoGeneratedComponents();

        }

        private void Simulate(SimulatedBody[] simulatedBodies)
        {
            // Add force to bodies
            foreach (SimulatedBody body in simulatedBodies)
            {

                float randomForceAmount = Random.Range(forceMinMax.x, forceMinMax.y);
                float forceAngle = ((randomizeForceAngle) ? Random.Range(0, 360f) : forceAngleInDegrees) * Mathf.Deg2Rad;
                Vector3 forceDir = new Vector3(Mathf.Sin(forceAngle), 0, Mathf.Cos(forceAngle));
                body.rigidbody.AddForce(forceDir * randomForceAmount, ForceMode.Impulse);
            }

            // Run simulation for maxIteration frames, or until all child rigidbodies are sleeping
            Physics.autoSimulation = false;
            for (int i = 0; i < maxIterations; i++)
            {
                Physics.Simulate(Time.fixedDeltaTime);

                if (simulatedBodies.All(body => body.rigidbody.IsSleeping()))
                {
                    break;
                }
            }
            Physics.autoSimulation = true;
        }

        // Automatically add rigidbody and box collider to object if it doesn't already have
        void AutoGenerateComponents(Transform[] gameObjects)
        {
            generatedRigidbodies = new List<Rigidbody>();
            generatedColliders = new List<Collider>();

            foreach (Transform child in gameObjects)
            {
                if (!child.GetComponent<Rigidbody>())
                {

                    Rigidbody rb = child.gameObject.AddComponent<Rigidbody>();

                    rb.useGravity = true;
                    rb.mass = 1;

                    generatedRigidbodies.Add(rb);

                }
                if (!child.GetComponent<Collider>())
                {
                    MeshCollider collider = child.gameObject.AddComponent<MeshCollider>();

                    collider.convex = true;

                    generatedColliders.Add(collider);
                }
            }
        }

        // Remove the components which were generated at start of simulation
        void RemoveAutoGeneratedComponents()
        {
            foreach (Rigidbody rb in generatedRigidbodies)
            {
                DestroyImmediate(rb);
            }
            foreach (Collider c in generatedColliders)
            {
                DestroyImmediate(c);
            }
        }

        public void UndoSimulation()
        {
            if (simulatedBodies != null)
            {
                foreach (SimulatedBody body in simulatedBodies)
                {
                    body.Undo();
                }
            }
        }

        struct SimulatedBody
        {
            public readonly Rigidbody rigidbody;

            readonly Geometry geometry;
            readonly Transform transform;

            public SimulatedBody(Transform transform)
            {
                this.transform = transform;
                this.rigidbody = transform.GetComponent<Rigidbody>();

                this.geometry = new Geometry(transform);
            }

            public void Undo()
            {
                // check if the transform was removed manually
                if (transform == null)
                    return;

                transform.position = geometry.getPosition();
                transform.rotation = geometry.getRotation();

                if (rigidbody != null)
                {
                    rigidbody.velocity = Vector3.zero;
                    rigidbody.angularVelocity = Vector3.zero;
                }
            }
        }
    }
}